/*
 * Copyright 2010 Kevin L'Huillier <klhuillier@gmail.com>
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package org.lhuillier.pwsafe.io.v3;


import java.util.Arrays;
import org.lhuillier.pwsafe.io.DecryptionFailedException;
import org.lhuillier.pwsafe.io.StretchedKey;
import org.lhuillier.pwsafe.io.codec.BinConverter;
import com.google.inject.Inject;


class DecryptImpl implements Decrypt {
    private static final int OFFSET_TAG = 0;
    private static final int LENGTH_TAG = 4;
    private static final int OFFSET_SALT = OFFSET_TAG + LENGTH_TAG;
    private static final int LENGTH_SALT = 32;
    private static final int OFFSET_ITER = OFFSET_SALT + LENGTH_SALT;
    private static final int LENGTH_ITER = 4;
    private static final int OFFSET_HP = OFFSET_ITER + LENGTH_ITER;
    private static final int LENGTH_HP = 32;
    private static final int OFFSET_B1 = OFFSET_HP + LENGTH_HP;
    private static final int LENGTH_KEY_BLOCKS = 16;
    private static final int OFFSET_IV = OFFSET_B1 + (LENGTH_KEY_BLOCKS * 4);
    private static final int LENGTH_IV = 16;
    private static final int OFFSET_HEADER = OFFSET_IV + LENGTH_IV;
    private static final int LENGTH_EOF = 16;
    private static final int LENGTH_HMAC = 32;
    private final Crypto crypto;
    private final BinConverter binConv;

    @Inject
    DecryptImpl(Crypto crypto, BinConverter binConv) {
        this.crypto = crypto;
        this.binConv = binConv;
    }

    @Override
    public byte[] decrypt(byte[] data, StretchedKey key)
            throws DecryptionFailedException {
        try {
            validateFile(data);
            byte[] stretchedKey = key.getKey();
            validateKey(data, stretchedKey);
            byte[] recordKey = decryptKey(data, stretchedKey, 0);
            byte[] hmacKey = decryptKey(data, stretchedKey, 2);
            byte[] decrypted = decryptRecords(data, recordKey);
            checkHmac(data, decrypted, hmacKey);
            return decrypted;
        } catch (DecryptionFailedException e) {
            throw e;
        } catch (Exception e) {
            throw new DecryptionFailedException("An error occurred while "
                    + "attempting to decrypt the database: " + e, e);
        }
    }

    /**
     * Checks the data length and for the PWS3 tag.
     * <p>
     * Length would normally be divisible by 16 as all blocks are 16 bytes.
     * However, the fields TAG and ITER are both 4 bytes, so subtract 8 before
     * verifying that the length is divisible by 16.
     */
    private void validateFile(byte[] data) throws DecryptionFailedException {
        int minSize = OFFSET_HEADER + 32 + LENGTH_EOF + LENGTH_HMAC;
        if (data.length < minSize || (data.length - 8) % 16 != 0
                || !"PWS3".equals(tag(data))) {
            throw new DecryptionFailedException("The data is not a valid "
                    + "PasswordSafe v3 database");
        }
    }

    /**
     * Verifies that the HMAC calculated from the decrypted records matches
     * the expected HMAC in the database.
     */
    private void checkHmac(byte[] data, byte[] decrypted, byte[] hmacKey)
            throws DecryptionFailedException {
        byte[] expected = hmac(data);
        byte[] actual = crypto.calculateHmac(decrypted, hmacKey);

        if (!Arrays.equals(expected, actual)) {
            throw new DecryptionFailedException(
                    "The database appears to be corrupt");
        }
    }

    /**
     * Calculates the SHA-256 value H(P') of the stretched password P'
     * and compares it to the value of H(P') in the database.
     */
    private void validateKey(byte[] data, byte[] stretchedPw)
            throws DecryptionFailedException {
        byte[] digest = crypto.digestKey(stretchedPw);
        byte[] expected = hp(data);

        if (!Arrays.equals(expected, digest)) {
            throw new DecryptionFailedException("Key is invalid");
        }
    }

    /**
     * The key from the passphrase is only used to encrypt two
     * randomly-generated keys, one for encrypting records, the other for
     * calculating the HMAC. This decrypts the generated keys.
     *
     * @param data
     * @param key the stretched key from the passphrase
     * @param index either 0 for the record key or 2 for the HMAC key
     * @return the decrypted 256-bit key
     */
    private byte[] decryptKey(byte[] data, byte[] key, int index) {
        int offset = OFFSET_B1 + (index * LENGTH_KEY_BLOCKS);
        return crypto.decryptKey(binConv.slice(data, offset, 32), key);
    }

    private byte[] decryptRecords(byte[] data, byte[] recordKey) {
        byte[] iv = iv(data);
        int nonRecordLen = OFFSET_HEADER + LENGTH_EOF + LENGTH_HMAC;
        int recordLen = data.length - nonRecordLen;
        byte[] records = binConv.slice(data, OFFSET_HEADER, recordLen);
        return crypto.decryptRecords(records, recordKey, iv);
    }

    private String tag(byte[] data) {
        return binConv.readString(data, OFFSET_TAG, LENGTH_TAG);
    }

    private byte[] hp(byte[] data) {
        return binConv.slice(data, OFFSET_HP, LENGTH_HP);
    }

    private byte[] iv(byte[] data) {
        return binConv.slice(data, OFFSET_IV, LENGTH_IV);
    }

    private byte[] hmac(byte[] data) {
        return binConv.slice(data, data.length - LENGTH_HMAC, LENGTH_HMAC);
    }
}
